16.5 释放
在运行时入口函数main.main里,会专门启动一个监控任务sysmon,它每隔一段时间就会检查heap里的闲置内存块。
proc.go
func sysmon() { scavengelimit:=int64(5601e9)
for{ usleep(delay)
if lastscavenge+scavengelimit/2<now{
mHeap_Scavenge(int32(nscavenge),uint64(now),uint64(scavengelimit))
lastscavenge=now
}
} }
遍历free、freelarge里的所有span,如闲置时间超过阈值,则释放其关联的物理内存。
mheap.go
func mHeap_Scavenge(k int32,now,limit uint64) { h:= &mheap_
// 遍历free数组里的所有链表 for i:=0;i<len(h.free);i++ { sumreleased+=scavengelist(&h.free[i],now,limit) }
// 遍历freelarge链表 sumreleased+=scavengelist(&h.freelarge,now,limit) }
func scavengelist(list*mspan,now,limit uint64)uintptr{ var sumreleased uintptr
// 遍历链表 for s:=list.next;s!=list;s=s.next{ // 检查闲置时间是否超出限制,而且内存没有全部被释放过 // 因为存在span合并的情况,所以有局部释放很正常 if(now-uint64(s.unusedsince)) >limit&&s.npreleased!=s.npages{ // 更新释放计数属性 released:= (s.npages-s.npreleased) << _PageShift sumreleased+=released s.npreleased=s.npages
// 释放内存
sysUnused((unsafe.Pointer)(s.start<<_PageShift),s.npages<<_PageShift)
}
} return sumreleased }
所谓物理内存释放,另有玄虚。
mem_linux.go
func sysUnused(v unsafe.Pointer,n uintptr) { madvise(v,n, _MADV_DONTNEED) }
系统调用madvise告知操作系统某段内存暂不使用,建议内核收回对应物理内存。当然,这只是一个建议,是否回收由内核决定。如物理内存资源充足,该建议可能会被忽略,以避免无谓的损耗。而当再次使用该内存块时,会引发缺页异常,内核会自动重新关联物理内存页。
分配器面对的是虚拟内存,所以在地址空间充足的情况下,根本无须放弃这段虚拟内存,无须收回mspan等管理对象,这也是arena能线性扩张的根本原因。
Microsoft Windows并不支持类似madvise的机制,须在获取span时主动补上被VirtualFree掉的内存。
mem_windows.go
func sysUnused(v unsafe.Pointer,n uintptr) { r:=stdcall3(_VirtualFree,uintptr(v),n, _MEM_DECOMMIT) }
func sysUsed(v unsafe.Pointer,n uintptr) { r:=stdcall4(_VirtualAlloc,uintptr(v),n, _MEM_COMMIT, _PAGE_READWRITE) }
mheap.go
func mHeap_AllocSpanLocked(h*mheap,npage uintptr) *mspan{ …
HaveSpan: // 如果被释放过物理内存,重新补上 if s.npreleased>0{ sysUsed((unsafe.Pointer)(s.start<<_PageShift),s.npages<<_PageShift) s.npreleased=0 }
return s }
多数UNIX-Like系统都支持madvise,所以它们的sysUsed函数大多什么都不做。
除周期性自动处理外,也可以调用runtime/debug.FreeOSMemory函数主动释放。